package com.minelx.landisk.filesystem.core.provider;

import com.minelx.landisk.filesystem.core.FileAccessingException;
import com.minelx.landisk.filesystem.core.values.PathDescription;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

class JavaFSPathMethodProvider implements PathMethodProvider {

	private final FileSystem fileSystem;

	private final String schemaAndAuthority;

	private final PathDescription description;

	private final Path path;

	JavaFSPathMethodProvider(FileSystem fileSystem, String schemaAndAuthority, PathDescription description) {
		this.fileSystem = fileSystem;
		this.schemaAndAuthority = schemaAndAuthority;
		this.description = description;
		// create path first
		path = createPath(fileSystem, description);
	}

	@Override
	public String uri() {
		return schemaAndAuthority + description.toText();
	}

	@Override
	public OutputStream openOutputStream(StandardOpenOption openOption) {
		try {
			return fileSystem.provider().newOutputStream(path, openOption);
		} catch (IOException e) {
			throw new FileAccessingException("error while obtaining stream.", e);
		}
	}

	@Override
	public InputStream openInputStream() {
		try {
			return fileSystem.provider().newInputStream(path);
		} catch (IOException e) {
			throw new FileAccessingException("error while obtaining stream.", e);
		}
	}

	@Override
	public void createFile() {
		if (path.toFile().exists()) {
			deleteFile();
		}
		try {
			OutputStream outputStream = fileSystem.provider().newOutputStream(path, StandardOpenOption.CREATE);
			outputStream.flush();
			outputStream.close();
		} catch (IOException e) {
			throw new FileAccessingException("error while creating file.", e);
		}
	}

	@Override
	public void deleteFile() {
		if (!path.toFile().exists()) {
			throw new FileAccessingException("path doesn't exists.");
		}
		try {
			fileSystem.provider().delete(path);
		} catch (IOException e) {
			throw new FileAccessingException("error while deleting file.", e);
		}
	}

	@Override
	public void createDirectory() {
		if (exists()) {
			deleteDirectory();
		}
		try {
			fileSystem.provider().createDirectory(path);
		} catch (IOException e) {
			throw new FileAccessingException("error while creating directory.", e);
		}
	}

	@Override
	public void deleteDirectory() {
		delete0(path.toFile());
	}

	@Override
	public boolean exists() {
		return path.toFile().exists();
	}

	private void delete0(File file) {
		// delete self if it's file
		if (file.isFile()) {
			doDelete(file);
			return;
		}
		// delete sub files first if it's a directory
		for (String childName : Objects.requireNonNull(file.list())) {
			delete0(new File(file.getAbsolutePath() + "/" + childName));
		}
		// delete itself.
		doDelete(file);
	}

	@Override
	public boolean isFile() {
		return path.toFile().isFile();
	}

	@Override
	public boolean isDirectory() {
		return path.toFile().isDirectory();
	}

	@Override
	public String[] listDir() {
		return path.toFile().list();
	}

	@Override
	public void rename(String name) {
		Path fileSystemPath = createPath(fileSystem, description.sibling(name));
		try {
			Files.move(path, fileSystemPath, REPLACE_EXISTING);
		} catch (IOException e) {
			throw new FileAccessingException("error while renaming file.", e);
		}
	}

	private static void doDelete(File file) {
		boolean delete = file.delete();
		if (!delete) {
			throw new FileAccessingException("file can't be deleted: " + file);
		}
	}

	private static Path createPath(FileSystem fileSystem, PathDescription pathDescription) {
		return fileSystem.getPath("", pathDescription.steps().toArray(new String[0]));
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		JavaFSPathMethodProvider that = (JavaFSPathMethodProvider) o;
		return fileSystem.equals(that.fileSystem) && path.equals(that.path) && description.equals(that.description);
	}

	@Override
	public int hashCode() {
		return Objects.hash(fileSystem, path, description);
	}
}